#include "renderer.h"

using namespace std;
using namespace Qt;
using namespace vmath;

//==============================================================================
// Public Interface
//==============================================================================

/**
 *	Creates a new instance of Renderer.
 */
Renderer::Renderer(QWidget* parent) : QGLWidget(parent)
{
    // Gives keyboard events
    setFocusPolicy(Qt::StrongFocus);

    initCamera();       // create and positions camera
    initAnimation();    // enable animation
    initAttributes();   // i.e. initialize this class
}

/**
 *	Destroys this Renderer instance.
 */
Renderer::~Renderer()
{
	// Nothing to do
}

/**
    Sets the current tool to be whatever is specified.
 */
void Renderer::setCurrentTool(VecTool tool)
{
    this->currentTool = tool;
}

//==============================================================================
// Event Handlers
//==============================================================================

/**
    Handles the event when the animation timer hits the
    end of a single interval.
*/
void Renderer::onTimerTick()
{
    this->time += TIME_INCREMENT;
    updateGL();

    this->aniTimer.start(ANIMATION_SPEED);
}

/**
 *	Handles the event when the OpenGL window size changes.
 */
void Renderer::onGLWindowResize( int w, int h )
{
	double  ratio;

	// Set up perspective viewing
	ratio = (float)w/(float)h;	
	glMatrixMode(GL_PROJECTION);
		glViewport(0,0,w,h);
		glLoadIdentity();
		gluPerspective(30, ratio, 1, 500);
	glMatrixMode(GL_MODELVIEW);

    positionOpenGLCamera();

    // Update the screen
	updateGL();
}


/**
 *	Handles the event when a mouse button is depressed.
 */
void Renderer::onMouseDown( QMouseEvent* _event )
{
    if (_event->buttons() == Qt::LeftButton)
    {
        switch (currentTool)
        {
            case VecTools::SELECT:
            {
                doSelectTool(MouseEvents::DOWN, _event);
                break;
            }
            case VecTools::PEN1:
            {
                doPen1Tool(MouseEvents::DOWN, _event);
                break;
            }
            case VecTools::PEN2:
            {
                doPen2Tool(MouseEvents::DOWN, _event);
                break;
            }
            case VecTools::CIRCLE:
            {
                doCircleTool(MouseEvents::DOWN, _event);
                break;
            }
        }
    }
    else if (_event->buttons() == Qt::RightButton)
    {
    }

    // Tracking mouse movement
    this->oldMouseX = _event->x();
    this->oldMouseY = _event->y();

    // Redraw the GL window
	updateGL();
}


/**
 *	Handles the event when the mouse moves while depressed.
 */
void Renderer::onMouseMove( QMouseEvent* _event )
{  
	/*int deltaX, deltaY;
    bool xNeg, yNeg;
    bool xNone, yNone;

    deltaX = _event->x() - this->oldMouseX;
    deltaY = _event->y() - this->oldMouseY;

    xNeg = deltaX < 0 ? true : false;
    yNeg = deltaY > 0 ? true : false;

    xNone = deltaX == 0.0;
    yNone = deltaY == 0.0;*/

    doHoverSelection(_event);

    if (_event->buttons() == Qt::LeftButton)
    {
        switch (currentTool)
        {
            case VecTools::SELECT:
            {
                doSelectTool(MouseEvents::MOVE, _event);
                break;
            }
            case VecTools::PEN1:
            {
                doPen1Tool(MouseEvents::MOVE, _event);
                break;
            }
            case VecTools::PEN2:
            {
                doPen2Tool(MouseEvents::MOVE, _event);
                break;
            }
            case VecTools::CIRCLE:
            {
                doCircleTool(MouseEvents::MOVE, _event);
                break;
            }
        }
    }
    else if (_event->buttons() == Qt::RightButton)
    {
    }

    this->oldMouseX = _event->x();
    this->oldMouseY = _event->y();

	updateGL();
}


/**
 *	Handles the event when a mouse button is released.
 */
void Renderer::onMouseUp( QMouseEvent* _event )
{  
    if (_event->buttons() == Qt::LeftButton)
    {
        switch (currentTool)
        {
            case VecTools::SELECT:
            {
                doSelectTool(MouseEvents::UP, _event);
                break;
            }
            case VecTools::PEN1:
            {
                doPen1Tool(MouseEvents::UP, _event);
                break;
            }
            case VecTools::PEN2:
            {
                doPen2Tool(MouseEvents::UP, _event);
                break;
            }
            case VecTools::CIRCLE:
            {
                doCircleTool(MouseEvents::UP, _event);
                break;
            }
        }
    }
    else if (_event->buttons() == Qt::RightButton)
    {
    }

	// Redraw the GL window
	updateGL();
}


/**
 *	Handles the event when the mouse scroll-wheel is used.
 */
void Renderer::onWheelScroll(QWheelEvent* _event)
{
    bool isDeltaNeg;

    isDeltaNeg = false;
    if (_event->delta() < 0)
    {
        isDeltaNeg = true;
    }

    // >>> INSERT STUFF HERE <<<

	updateGL();
	_event->accept();
}

/**
    Handles the event when a keyboard key is pressed.
*/
void Renderer::onKeyDown(QKeyEvent* _event)
{
    const int ENTER = 16777220;

    switch(_event->key())
    {
        // >>> INSERT STUFF HERE <<<
        default:
        {
            QGLWidget::keyPressEvent(_event);
        }
    }
}

//==============================================================================
// OpenGL Rendering
//==============================================================================

/**
 *	Set up the OpenGL rendering environment.
 */
void Renderer::paintGL()
{
    list<Graphic*>::iterator gIter;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  

    if (this->newModel == true)
    {
        positionOpenGLCamera();
        this->newModel = false;
    }    

    // draw all of the graphics
    if (this->graphics.size() > 0)
    {
        // render the selected vertex differently
        if (this->vertNearMouse != NULL)
        {
            glPointSize(7.0);
            glColor3f(0.0, 0.0, 1.0);
            glBegin(GL_POINTS);
                glVertex3dv(&this->vertNearMouse->onCurvePoint[0]);
            glEnd();
        }

        // draw the control points/lines
        gIter = this->graphics.begin();
        do
        {
            if ((*gIter)->points.size() > 0)
            {
                (*gIter)->drawControlGeometry();
            }

            gIter++;
        }
        while(gIter != this->graphics.end());

        // draw the actual shapes
        gIter = this->graphics.end();
        gIter--;
        do
        {
            if ((*gIter)->points.size() > 0)
            {
                (*gIter)->draw();
            }

            if (gIter == this->graphics.begin())
            {
                gIter = this->graphics.end();
            }
            else
            {
                gIter--;
            }
        }
        while(gIter != this->graphics.end());
    }
}

//==============================================================================
// Initializations
//==============================================================================

/**
 *	Sets up the OpenGL rendering environment.
 */
void Renderer::initOpenGL()
{
    glClearColor (0.0, 1.0, 1.0, 1.0);

    // Basic drawing config
    glPointSize(5.0);
    glLineWidth(1.0);

    // Enable stuff we need
    glEnable(GL_LIGHTING);          // lighting
    glEnable(GL_DEPTH_TEST);        // polygon depth tests
    glEnable(GL_COLOR_MATERIAL);    // surface colors
    glEnable(GL_BLEND);

    // Rendering config
    glShadeModel(GL_SMOOTH);                // per-vertex shading
    glBlendFunc(GL_SRC_ALPHA, 
                GL_ONE_MINUS_SRC_ALPHA);    

    // Set up some lighting
    GLfloat light_position[] = { 1.0, 1.0, 1.0, 0.0 };
    glLightfv(GL_LIGHT0, GL_POSITION, light_position);
    glEnable(GL_LIGHT0);

    // Set material defaults
    GLfloat mat_specular[]  = { 1.0, 1.0, 1.0, 1.0 };
    GLfloat mat_shininess[] = { 50.0 };
    glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular);
    glMaterialfv(GL_FRONT, GL_SHININESS, mat_shininess);

    // Load a dummy texture
    loadTexture("nothing", 128, 128);
}

/**
    #TODO explain what his function does
*/
void Renderer::initAnimation()
{
    connect(&this->aniTimer,    SIGNAL(timeout()),      // connect timer timeout event
            this,               SLOT(onTimerTick()));   // with handler in this class
}

/**
    #TODO explain what his function does
*/
void Renderer::initCamera()
{
    this->camera.radius     = CAMERA_RADIUS;    // set camera rotation radius
    this->camera.angle      = 90;               // rotate camer by X degrees
    this->camera.height     = 0.0;              // camera is this high 

    this->camera.updatePosition();          // tell camer to update its position
                                            // (doesn't update the GL state)
}

/**
    #TODO explain what his function does
*/
void Renderer::initAttributes()
{
    this->newModel          = true;
    this->currentGraphic    = NULL;
    this->currentVertex     = NULL;
    this->vertNearMouse     = NULL;
    this->currentTool       = VecTools::PEN1;
}

//==============================================================================
// Helper Functions
//==============================================================================

int Renderer::nearestPowerOf2(int n)
{
    int exp = (int)ceil(log10((float)n)/log10((float)2));
    return (int)pow(2.0f, exp);
}

/**
   Loads an image file into texture memory. Requires converting to GL 
   texture format. Texture dimensions must be powers of 2 (32, 64, 
   128, etc.)
*/
GLuint Renderer::loadTexture(QString fname, int dWidth, int dHeight)
{
    QImage tex1, buf;
    
    // Load the image from file
    if ( !buf.load( fname ) ) {	
    	// If file wasn't loaded, initialize texture to some default state
		qWarning( "Could not read image file, using single-color instead." );
		QImage dummy(128, 128, QImage::Format_RGB32);
		dummy.fill(0);
		buf = dummy;
    }
    
    // If no desired width given, use the image dimensions
    if (dWidth == -1) dWidth = buf.width();
    if (dHeight == -1) dHeight = buf.height();
    
    // Make sure the desired dimensions are powers of 2
    dWidth = nearestPowerOf2(dWidth);
    dHeight = nearestPowerOf2(dHeight);
    
    // Resize to power-of-2 dimensions and convert to OpenGL format
    tex1 = buf;
    //tex1 = buf.scaledToHeight(dHeight);
    //tex1 = tex1.scaledToWidth(dWidth);
    tex1 = QGLWidget::convertToGLFormat( tex1 );
    
    // Get a unique texture name
	GLuint texName;
	glGenTextures(1, &texName);
    glBindTexture(GL_TEXTURE_2D, texName);
    
    // Set texture parameters
    glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    
    // Load texture data
    glTexImage2D( GL_TEXTURE_2D, 0, 3, tex1.width(), tex1.height(), 0,
		  GL_RGBA, GL_UNSIGNED_BYTE, tex1.bits() );

	return texName;
}

/**
    Updates the camera's position in the OpenGL state.
*/
void Renderer::positionOpenGLCamera()
{
    Camera& cam = this->camera;
    glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
    gluLookAt(cam.eye[0], cam.eye[1], cam.eye[2], 
              cam.at[0], cam.at[1], cam.at[2], 
              cam.up[0], cam.up[1], cam.up[2]);
}

/**
    Returns a random vector with each axis component within the
    specified limit. If not specitied, the limit is 15 units.
*/
vec3d Renderer::getRandomVector(bool obeyBounds, double randLimit)
{
    const double RAND_CEIL = 15.0;
    int sign;
    vec3d result;

    // set the maximum random number
    if (randLimit == 0.0)
    {
        randLimit = RAND_CEIL;
    }

    // generate random coordinates
    result = vec3d((((double)(rand()%100))/100.0) * randLimit, 
                   (((double)(rand()%100))/100.0) * randLimit, 
                   (((double)(rand()%100))/100.0) * randLimit);

    for (int i = 0; i < 3; i++)
    {
        sign = rand()%2; 
        sign = sign == 0? -1 : 1;
        result[i] = result[i] * sign;
    }

    // do bounds checking if enabled
    if (obeyBounds)
    {
        // perform bounds check
    }

    return result;
}

/**
    The specified window point as a vertex in 3D
    world space.
*/
vec3d Renderer::getCanvasPoint( const vec3d& windowPoint)
{
    double  oldZ;
    double  zRatio;
    vec3d   rayOrigin;
    vec3d   rayDirection;

    unprojectRay(windowPoint, rayOrigin, rayDirection);

    oldZ = rayDirection[2];
    zRatio = abs(rayOrigin[2] / oldZ);
    rayDirection *= zRatio;

    return rayOrigin + rayDirection;
}

/**	
    Creates a ray that originates from the camera and
    travels through the specified window point.
 */
void Renderer::unprojectRay(const vec3d& windowPoint, 
								  vec3d& rayOrigin, 
								  vec3d& rayDirection)
{
	rayOrigin = windowPoint;
	unprojectPoint(rayOrigin, 0.0);

	rayDirection = windowPoint;
	unprojectPoint(rayDirection, 1.0);
	rayDirection -= rayOrigin;
    rayDirection = vmath::normalize(rayDirection);
}

/**	Unprojects the specified point from the window to a viewing plane
 *	that is some distance away from the near plane.
 */
void Renderer::unprojectPoint(vec3d& windowPoint, 
							  const double distance)
{
	const unsigned x = 0, y = 1, z = 2;

	double   pX, pY, pZ;
	GLint    vp[4];  
	GLdouble mvmatrix[16];
	GLdouble projmatrix[16];

	// Get viewport info: [x, y, width, height]
	glGetIntegerv(GL_VIEWPORT, vp);
	
	// Get the modelview and projection matrices
	glGetDoublev(GL_MODELVIEW_MATRIX, mvmatrix);
	glGetDoublev(GL_PROJECTION_MATRIX, projmatrix);
	
	// unprojecting onto the near plane
	gluUnProject(windowPoint[x], vp[3] - windowPoint[y], distance, 
		         mvmatrix, projmatrix, vp, 
				 &pX, &pY, &pZ);

	windowPoint[x] = pX;
	windowPoint[y] = pY;
	windowPoint[z] = pZ;
}

/**
    #TODO Describe what this function does.
*/
void Renderer::doSelectTool(MouseEvent eventType, QMouseEvent* mouseEvent )
{
}

/**
    #TODO Describe what this function does.
*/
void Renderer::doPen1Tool(MouseEvent eventType, QMouseEvent* mouseEvent )
{
    Vertex*         newVertex;
    vec3d           newPoint;
    vec3d           distance;
    QuadraticCurve* newGraphic;

    switch (eventType)
    {
        case MouseEvents::DOWN:
        {
            if (this->vertNearMouse == NULL ||
                this->currentGraphic == NULL)
            {
                // get the mouse point in 3D space
                newPoint = getCanvasPoint(vec3d(mouseEvent->x(), mouseEvent->y(), 0.0));

                // create a new graphic
                if (currentGraphic == NULL)
                {
                    newGraphic = new QuadraticCurve();
                    this->graphics.push_back(newGraphic);
                    this->currentGraphic = newGraphic;
                }
                newVertex = this->currentGraphic->addPoint(newPoint);
                this->currentVertex = newVertex;
            }
            else
            {
                // if vertex near mouse is the first in the curve
                if (this->vertNearMouse == &this->currentGraphic->points.front())
                {
                    // current graphic, close the curve
                    this->currentGraphic->close();
                    this->currentGraphic = NULL;
                    this->currentVertex = NULL;
                }
            }

            break;
        }
        case MouseEvents::MOVE:
        {
            if (this->currentVertex != NULL &&
                this->currentGraphic != NULL)
            {
                newPoint = getCanvasPoint(vec3d(mouseEvent->x(), mouseEvent->y(), 0.0));
                distance = currentVertex->onCurvePoint - newPoint;
                currentVertex->nextOffCurvePoint = newPoint;
                currentVertex->prevOffCurvePoint = currentVertex->onCurvePoint + distance;

                this->currentGraphic->controlUpdated(currentVertex);
            }

            break;
        }
        case MouseEvents::UP:
        {
            break;
        }
    }
}

/**
    #TODO Describe what this function does.
*/
void Renderer::doPen2Tool(MouseEvent eventType, QMouseEvent* mouseEvent )
{
}

/**
    #TODO Describe what this function does.
*/
void Renderer::doCircleTool(MouseEvent eventType, QMouseEvent* mouseEvent )
{
}

/**
    Checks if the mouse is hovering over a point on the current
    graphic being drawn.
*/
void Renderer::doHoverSelection(QMouseEvent* e)
{
    const double MAX_DISTANCE = 1.0;

    bool hoverSuccess;
    vec3d mouseOnCanvas;
    list<Vertex>::iterator vIter;

    hoverSuccess = false;

    // TODO convert mouse point to canvas point
    mouseOnCanvas = getCanvasPoint(vec3d(e->x(), e->y(), 0.0));

    if (this->currentGraphic != NULL)
    {
        // check points of current graphic
        vIter = this->currentGraphic->points.begin();
        do
        {
            // check point proximity
            if (vmath::length(mouseOnCanvas - (*vIter).onCurvePoint) < MAX_DISTANCE)
            {
                hoverSuccess = true;
                this->vertNearMouse = &(*vIter);
                vIter = this->currentGraphic->points.end(); // to end the loop
            }
            else
            {
                vIter++;
            }
        }
        while(vIter != this->currentGraphic->points.end());
    }
    else
    {
        // check points of all graphics
    }

    if (!hoverSuccess)
    {
        this->vertNearMouse = NULL;
    }
}